#include "const.h"

#define MAX_LEXAN 32768
static char g_conf_lexan[MAX_LEXAN];

enum {
	TOKEN_EOF=0,

	TOKEN_NEWLINE='\n',
	TOKEN_ASSIGN='=',

	TOKEN_COMMA=',',

	TOKEN_LPAREN='(',
	TOKEN_RPAREN=')',

	__TOKEN_START=256,

	TOKEN_ADD_ASSIGN,

	TOKEN_IDENT,
	TOKEN_STRING
};

enum {
	__FN_START=0,

	FN_SHELL,			// shell("commandline...")
	FN_FOREACH,			// foreach(vararrayptr, "expansion")
	FN_WILDCARD,		// wildcard("somedir/*.ext")

	__FN_COUNT
};

////////////////////////////////////////////////////////////////////////////////

static const char *conf_tokenname(int token) {
	switch(token) {
	case TOKEN_EOF:			return "end of file";
	case TOKEN_NEWLINE:		return "newline";
	case TOKEN_ASSIGN:		return "'='";
	case TOKEN_COMMA:		return "','";
	case TOKEN_LPAREN:		return "'('";
	case TOKEN_RPAREN:		return "')'";
	case TOKEN_ADD_ASSIGN:	return "'+='";
	case TOKEN_IDENT:		return "identifier";
	case TOKEN_STRING:		return "string";
	default:				return "(don't know)";
	}
}

static int conf_lex(buffer_t b) {
	size_t i;
	char c;
	int incomment;

	g_conf_lexan[i = 0] = 0;
	incomment = 0;

__start:
	if (!(c = b_read(b)))
		return TOKEN_EOF;

	if (c=='\r') {
		if (b_look(b)=='\n')
			b_read(b);

		c = '\n';
	}

	// newline?
	if (c=='\n') {
		strcpy(g_conf_lexan, "\n");
		return TOKEN_NEWLINE;
	}

	// in a comment?
	if (incomment)
		goto __start;

	// skip whitespace
	if (c<=' ')
		goto __start;

	// skip comments
	if (c=='#' || (c=='/'&&b_look(b)=='/')) {
		incomment = 1;
		goto __start;
	}

	// assignment?
	if (c=='=')
		return TOKEN_ASSIGN;

	// parentheses?
	if (c=='(' || c==')')
		return c;

	// comma?
	if (c==',')
		return TOKEN_COMMA;

	// assignment operator?
	if (c=='+' && b_look(b)=='=') {
		b_read(b);
		strcpy(g_conf_lexan, "+=");
		return TOKEN_ADD_ASSIGN;
	}

	// identifier?
	if ((c>='a'&&c<='z') || (c>='A'&&c<='Z') || c=='_' || c=='.') {
		while(1) {
			if (i==sizeof(g_conf_lexan)-1) {
				b_error(b, "lexan exceeds %u characters\n",
					sizeof(g_conf_lexan));
			}

			g_conf_lexan[i++] = c;

			c = b_look(b);
			if (!((c>='a'&&c<='z') || (c>='A'&&c<='Z') || (c>='0'&&c<='9')
			|| c=='_' || c=='.')) {
				g_conf_lexan[i] = 0;
				return TOKEN_IDENT;
			}

			b_read(b);
		}
	}

	// string?
	if (c=='\"') {
		c = b_read(b);

		while(c != '\"') {
			// escape sequence
			if (c=='\\') {
				c = b_read(b);

				switch(c) {
				case 'a':	g_conf_lexan[i++] = '\a'; break; //alert
				case 'b':	g_conf_lexan[i++] = '\b'; break; //backspace
				case 'e':	g_conf_lexan[i++] = 27;   break; //ESC
				case 'f':	g_conf_lexan[i++] = '\f'; break; //formfeed
				case 'n':	g_conf_lexan[i++] = '\n'; break; //newline
				case 'r':	g_conf_lexan[i++] = '\r'; break; //carriage return
				case 't':	g_conf_lexan[i++] = '\t'; break; //tab
				case 'v':	g_conf_lexan[i++] = '\v'; break; //vertical tab
				case '\'':	g_conf_lexan[i++] = '\''; break; //single quote
				case '\"':	g_conf_lexan[i++] = '\"'; break; //double quote
				case '\\':	g_conf_lexan[i++] = '\\'; break; //backslash
				case '\?':	g_conf_lexan[i++] = '\?'; break; //question mark
				case '\n':	break;                           //continuation
				case '\0':	b_error(b, "unexpected EOF in escape sequence");
				default:	b_error(b, "unknown escape sequence '\\%c'", c);
				}

				c = b_read(b);
				continue;
			}

			// error handling
			if (!c || c=='\n')
				b_error(b, "unexpected string termination");

			// normal character
			g_conf_lexan[i++] = c;

			// read the next character
			c = b_read(b);
		}

		g_conf_lexan[i] = 0;
		return TOKEN_STRING;
	}

	// wtf is this then?
	b_error(b, "stray character in program '%c' (0x%.2X)", c, (unsigned int)c);
	return 0;
}

// expect a specific token
static void conf_expected(buffer_t b, int t, int g) {
	b_error(b, "expected %s, got %s", conf_tokenname(t), conf_tokenname(g));
}
static void conf_expect(buffer_t b, int t) {
	int token;

	token = conf_lex(b);
	if (token!=t)
		conf_expected(b, t, token);
}

// expect one of the following tokens
static int conf_expectany(buffer_t b, int g, ...) {
	const char *prev;
	va_list args;
	char explist[512];
	int t;

	explist[0] = 0;
	prev = "";

	va_start(args, g);
	while((t = va_arg(args, int)) != 0) {
		i_strcat(explist, sizeof(explist), prev);
		i_strcat(explist, sizeof(explist), conf_tokenname(t));
		prev = " or ";

		if (g==t)
			return g;
	}
	va_end(args);

	b_error(b, "expected %s, got %s", explist, conf_tokenname(g));
	return 0;
}

// find a function
static int conf_findfunc(buffer_t b, const char *func) {
	if(!strcmp(func, "shell"))		return FN_SHELL;
	if(!strcmp(func, "foreach"))	return FN_FOREACH;
	if(!strcmp(func, "wildcard"))	return FN_WILDCARD;

	b_error(b, "unknown function '%s'", func);
	return 0;
}

// handle a shell() invocation
static void func_shell(buffer_t b, var_t v, v_assign_fn_t vassignfn,
const char *command) {
	static char buf[65536];
	static char tmp[8192];
	FILE *f;
	int l;

	if (!(f = popen(command, "r"))) {
		b_error(b, "failed to run shell command \"%s\"; %s",
			command, i_errnostring());
		return;
	}

	buf[0] = 0;
	while(!feof(f)) {
		if (fgets(tmp, sizeof(tmp)-1, f)) {
			l = strlen(tmp);
			if (l > 0) {
				if (tmp[l-1]!='\n') {
					tmp[l] = '\n';
					tmp[l+1] = 0;
				}
			}
			i_strcat(buf, sizeof(buf), tmp);
		} else
			b_error(b, "failed to read from process; %s", i_errnostring());
	}

	if (pclose(f)==-1) {
		b_error(b, "an error occurred while running the shell; %s",
			i_errnostring());
	}

	vassignfn(v, buf);
}

// handle a foreach() invocation
struct foreach_s {
	var_t dst;
	const char *exp;
};
static int __int_foreach(var_t v, size_t i, void *p) {
	struct foreach_s *fes;

	if (v||i) {/*unused*/}
	fes = (struct foreach_s *)p;

	v_cat(fes->dst, v_format(fes->exp));
	return 1;
}
static void func_foreach(buffer_t b, var_t v, v_assign_fn_t vassignfn,
var_t vararray, const char *expansion) {
	struct foreach_s fes;

	if (b) {/*unused*/}

	if (vassignfn==v_set)
		v_set(v, "");

	fes.dst = v;
	fes.exp = expansion;

	v_foreach(vararray, __int_foreach, &fes);
}

// handle a wildcard() invocation
static void func_wildcard(buffer_t b, var_t v, v_assign_fn_t vassignfn,
const char *pattern) {
	if (b) {/*unused*/}

	if (vassignfn==v_set)
		v_set(v, "");

	wildcard(v, pattern);
}

// invoke a function
static void conf_dofunc(buffer_t b, var_t v, v_assign_fn_t vassignfn, int fn) {
	var_t vararray;

	conf_expect(b, TOKEN_LPAREN);

	switch(fn) {
	// shell("command line...")
	case FN_SHELL:
		conf_expect(b, TOKEN_STRING);
		func_shell(b, v, vassignfn, g_conf_lexan);
		break;

	// foreach(vararrayptr, "expansion")
	case FN_FOREACH:
		conf_expect(b, TOKEN_IDENT);
		vararray = v_findarray(v_var(g_conf_lexan));
		if (!vararray) {
			b_error(b, "could not find an array associated with \"%s\"",
				g_conf_lexan);
		}
		conf_expect(b, TOKEN_COMMA);
		conf_expect(b, TOKEN_STRING);
		if (vararray)
			func_foreach(b, v, vassignfn, vararray, g_conf_lexan);
		break;

	// wildcard("somedir/*.ext")
	case FN_WILDCARD:
		conf_expect(b, TOKEN_STRING);
		func_wildcard(b, v, vassignfn, g_conf_lexan);
		break;
	}

	conf_expect(b, TOKEN_RPAREN);
}

////////////////////////////////////////////////////////////////////////////////

int readconf(const char *filename) {
#define OS_RESTRICT_BIT 0x01
#define ARCH_RESTRICT_BIT 0x02
#define MODE_RESTRICT_BIT 0x04
	v_assign_fn_t vassignfn;
	buffer_t b;
	var_t v;
	char resolvedpath[256];
	char temp[256];
	int restricted;
	int token;

	b = b_open(filename);

	restricted = 0;

	v_setvar("file", filename);
	v_setvar("filepath", i_realpath(filename,
		resolvedpath, sizeof(resolvedpath)));

	while(1) {
		// grab the token, break on EOF
		if ((token = conf_lex(b))==TOKEN_EOF)
			break;

		// ignore initial newlines
		if (token==TOKEN_NEWLINE)
			continue;

		// make sure the token is an identifier
		if (token!=TOKEN_IDENT) {
			if (!restricted) {
				conf_expected(b, TOKEN_IDENT, token);
				b_close(b);

				return 0;
			}

			continue;
		}

		// handle os-dependent stuff
		if (!strcmp(g_conf_lexan, "os_dependent")
		&& (restricted&OS_RESTRICT_BIT || !restricted)) {
			// perform actions only on a specific target operating system
			conf_expect(b, TOKEN_STRING);

			if (!strcmp(v_format(g_conf_lexan), v_getvar("targetsys")))
				restricted &= ~OS_RESTRICT_BIT;
			else
				restricted |= OS_RESTRICT_BIT;

			conf_expect(b, TOKEN_NEWLINE);
			continue;
		} else if(!strcmp(g_conf_lexan, "arch_dependent")
		&& (restricted&ARCH_RESTRICT_BIT || !restricted)) {
			// perform actions only on a specific target architecture
			conf_expect(b, TOKEN_STRING);

			if (!strcmp(v_format(g_conf_lexan), v_getvar("targetarch")))
				restricted &= ~ARCH_RESTRICT_BIT;
			else
				restricted |= ARCH_RESTRICT_BIT;

			conf_expect(b, TOKEN_NEWLINE);
			continue;
		} else if(!strcmp(g_conf_lexan, "runmode_dependent")
		&& (restricted&MODE_RESTRICT_BIT || !restricted)) {
			// perform actions only for a specific run-mode
			conf_expect(b, TOKEN_STRING);

			if (!strcmp(v_format(g_conf_lexan), v_getvar("targetmode")))
				restricted &= ~MODE_RESTRICT_BIT;
			else
				restricted |= MODE_RESTRICT_BIT;

			conf_expect(b, TOKEN_NEWLINE);
			continue;
		}

		// is code execution restricted due to dependencies?
		if (restricted) {
			if (!strcmp(g_conf_lexan, "os_independent")
			&& restricted==OS_RESTRICT_BIT)
				restricted &= ~OS_RESTRICT_BIT;
			else if(!strcmp(g_conf_lexan, "arch_independent")
			&& restricted==ARCH_RESTRICT_BIT)
				restricted &= ~ARCH_RESTRICT_BIT;
			else if(!strcmp(g_conf_lexan, "mode_independent")
			&& restricted==MODE_RESTRICT_BIT)
				restricted &= ~MODE_RESTRICT_BIT;
			else if(!strcmp(g_conf_lexan, "independent"))
				restricted = 0;

			if (!restricted)
				conf_expect(b, TOKEN_NEWLINE);

			continue;
		}

		// is this an instruction?
		if (!strcmp(g_conf_lexan, "echo")) {
			// write text to stdout
			conf_expect(b, TOKEN_STRING);
			fprintf(stdout, "%s\n", v_format(g_conf_lexan));
			fflush(stdout);
			conf_expect(b, TOKEN_NEWLINE);
			continue;
		} else if(!strcmp(g_conf_lexan, "verbose")) {
			// write text to stdout if in verbose mode
			conf_expect(b, TOKEN_STRING);
			if (isverbose()) {
				fprintf(stdout, "%s\n", v_format(g_conf_lexan));
				fflush(stdout);
			}
			conf_expect(b, TOKEN_NEWLINE);
			continue;
		} else if(!strcmp(g_conf_lexan, "warn")) {
			// write a warning to stderr
			conf_expect(b, TOKEN_STRING);
			fprintf(stderr, "warning: %s:%u: %s\n", b->filename, b->line,
				v_format(g_conf_lexan));
			fflush(stderr);
			conf_expect(b, TOKEN_NEWLINE);
			continue;
		} else if(!strcmp(g_conf_lexan, "rm")) {
			// delete a file
			conf_expect(b, TOKEN_STRING);
			if (remove(v_format(g_conf_lexan)) != 0) {
				fprintf(stderr, "warning: %s:%u: in remove(\"%s\"): %s\n",
					b->filename, b->line, v_format(g_conf_lexan),
					i_errnostring());
				fflush(stderr);
			}
			conf_expect(b, TOKEN_NEWLINE);
			continue;
		} else if(!strcmp(g_conf_lexan, "mkdir")) {
			// create a directory (or series of directories) if they don't exist
			conf_expect(b, TOKEN_STRING);
			mkdirs(v_format(g_conf_lexan));
			conf_expect(b, TOKEN_NEWLINE);
			continue;
		} else if(!strcmp(g_conf_lexan, "shell")) {
			// run a shell command
			conf_expect(b, TOKEN_STRING);
			system(v_format(g_conf_lexan));
			conf_expect(b, TOKEN_NEWLINE);
			continue;
		} else if(!strcmp(g_conf_lexan, "array")) {
			// declare an array
			conf_expect(b, TOKEN_IDENT);
			i_strcpy(temp, sizeof(temp), g_conf_lexan);
			conf_expect(b, TOKEN_COMMA);
			conf_expect(b, TOKEN_IDENT);
			v_new_array(temp, g_conf_lexan, 16);
			conf_expect(b, TOKEN_NEWLINE);
			continue;
		} else if(!strcmp(g_conf_lexan, "os_independent")
		|| !strcmp(g_conf_lexan, "arch_independent")
		|| !strcmp(g_conf_lexan, "mode_independent")
		|| !strcmp(g_conf_lexan, "independent")) {
			if (!strcmp(v_getvar("opt.verbose"), "yes")) {
				fprintf(stderr, "warning: %s:%u: already independent\n",
					b->filename, b->line);
				fflush(stderr);
			}

			conf_expect(b, TOKEN_NEWLINE);
			continue;
		}

		// the identifier token marks a key, save it
		v = v_var(g_conf_lexan);

		// need an assignment
		if (!(token = conf_expectany(b, conf_lex(b),
		TOKEN_ASSIGN, TOKEN_ADD_ASSIGN, 0)))
			return 0;

		// select the assignment function
		vassignfn = token==TOKEN_ASSIGN ? v_set : v_cat;

		// need an identifier or a string
		if (!(token = conf_expectany(b, conf_lex(b),
		TOKEN_IDENT, TOKEN_STRING, 0)))
			return 0;

		switch(token) {
		// function call
		case TOKEN_IDENT:
			conf_dofunc(b, v, vassignfn, conf_findfunc(b, g_conf_lexan));
			break;
		// string
		case TOKEN_STRING:
			vassignfn(v, v_format(g_conf_lexan));
			break;
		// not yet handled
		default:
			i_interror("case not handled");
			break;
		}

		token = conf_lex(b);
		if (token!=TOKEN_EOF && token!=TOKEN_NEWLINE) {
			conf_expected(b, TOKEN_NEWLINE, token);
			b_close(b);

			return 0;
		}
	}

	b_close(b);
	return 1;
}
